﻿// <copyright file="DataBlock.cs" company="Sinclair Community College">
// Copyright 2010 Sinclair Community College
// </copyright>
// <originalAuthor>Patrick S. Seymour (patrick.seymour@gmail.com)</originalAuthor>

namespace SinclairCC.AppV
{
    using System;
    using System.Diagnostics.CodeAnalysis;

    /// <summary>
    /// This class represents one block of data from a file stored in the SFT file.
    /// </summary>
    public class DataBlock : HeaderMetadataSection
    {
        /// <summary>
        /// This field is reserved and is initialized to all zeroes.
        /// </summary>
        [SuppressMessage("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields", Justification = "This field is marked as reserved in the App-V File Format Specification.")]
        private byte[] reserved;

        /// <summary>
        /// The GUID of a directory entry to which this block belongs.
        /// </summary>
        private Guid directoryEntryId;

        /// <summary>
        /// Stores the Feature Block (FB) number to which this block belongs.
        /// </summary>
        /// <remarks>This value must be initialized to either one (for FB1 files)
        /// or two (for FB2 files).</remarks>
        private short featureBlockNumber;

        /// <summary>
        /// The relative offset inside the file - to which this block belongs - for
        /// the data contained within the data block.
        /// </summary>
        private int dataOffset;

        /// <summary>
        /// The size of the data contained within the data block.
        /// </summary>
        /// <remarks>
        /// If compression is used, this field will reflect the uncompressed size of the
        /// data block’s content. If a block size of 64 kilobytes (0x10000 bytes) is
        /// used for the SFT package, it is stored as 0x0000 in this field.
        /// </remarks>
        private ushort dataSize;

        /// <summary>
        /// The actual size of the data that begins immediately after this field in the
        /// data block's content part.
        /// </summary>
        /// <remarks>
        /// If compression is used, this field will reflect the compressed size of the
        /// data block's content. If a block size of 64 kilobytes (0x10000 bytes) is used
        /// for the SFT package, it is stored as 0x0000 in this field.
        /// </remarks>
        private ushort physicalDataSize;

        /// <summary>
        /// The position in the file of the actual (non-header) data for this data block.
        /// </summary>
        private long startPositionOfData;

        /// <summary>
        /// Initializes a new instance of the DataBlock class.
        /// </summary>
        /// <param name="fileReader">
        /// A binary file reader that points to the SFT file.
        /// </param>
        public DataBlock(System.IO.BinaryReader fileReader)
            : base(fileReader)
        {
            this.startPositionOfData = -1;
        }

        /// <summary>
        /// Gets the GUID of a directory entry to which this block belongs.
        /// </summary>
        public Guid DirectoryEntryId
        {
            get
            {
                this.ReadSection();
                return this.directoryEntryId;
            }
        }

        /// <summary>
        /// Gets the Feature Block (FB) number to which this block belongs.
        /// </summary>
        /// <remarks>This value must be initialized to either one (for FB1 files)
        /// or two (for FB2 files).</remarks>
        public short FeatureBlockNumber
        {
            get
            {
                this.ReadSection();
                return this.featureBlockNumber;
            }
        }

        /// <summary>
        /// Gets the relative offset inside the file - to which this block belongs - for
        /// the data contained within the data block.
        /// </summary>
        public int DataOffset
        {
            get
            {
                this.ReadSection();
                return this.dataOffset;
            }
        }

        /// <summary>
        /// Gets the size of the data contained within the data block.
        /// </summary>
        /// <remarks>
        /// If compression is used, this field will reflect the uncompressed size of the
        /// data block’s content. If a block size of 64 kilobytes (0x10000 bytes) is
        /// used for the SFT package, it is stored as 0x0000 in this field.
        /// </remarks>
        public int DataSize
        {
            get
            {
                this.ReadSection();

                // If a block size of 64 KB (0x10000 bytes) is used for the SFT package, it
                // is stored as 0x0000 in this field. So if there is a zero value in this
                // field, return 64 KB as the size. Otherwise, return the size specified in
                // the SFT file.
                if (this.dataSize == 0)
                {
                    return 64 * 1024;
                }
                else
                {
                    return this.dataSize;
                }
            }
        }

        /// <summary>
        /// Gets the actual size of the data that begins immediately after this field in the
        /// data block's content part.
        /// </summary>
        /// <remarks>
        /// If compression is used, this field will reflect the compressed size of the
        /// data block's content. If a block size of 64 kilobytes (0x10000 bytes) is used
        /// for the SFT package, it is stored as 0x0000 in this field.
        /// </remarks>
        public int PhysicalDataSize
        {
            get
            {
                this.ReadSection();

                // If a block size of 64 KB (0x10000 bytes) is used for the SFT package, it
                // is stored as 0x0000 in this field. So if there is a zero value in this
                // field, return 64 KB as the size. Otherwise, return the size specified in
                // the SFT file.
                if (this.physicalDataSize == 0)
                {
                    return 64 * 1024;
                }
                else
                {
                    return this.physicalDataSize;
                }
            }
        }

        /// <summary>
        /// Writes the bytes from this data block to the specified BinaryWriter, using the
        /// given compression method.
        /// </summary>
        /// <param name="fileWriter">
        /// The BinaryWriter object to which the bytes are written.
        /// </param>
        /// <param name="compression">
        /// The compression method to be used on the data block's bytes before writing to the file.
        /// </param>
        public void WriteBytesToFile(System.IO.BinaryWriter fileWriter, CompressionMethod compression)
        {
            this.ReadSection();

            this.FileReader.BaseStream.Seek(this.startPositionOfData, System.IO.SeekOrigin.Begin);

            if (compression != CompressionMethod.None)
            {
                System.IO.BinaryReader decompressedReader = Compression.GetDecompressedReader(this.FileReader, this.PhysicalDataSize, compression);

                byte[] buffer = new byte[1024];
                int bytesRead = -1;
                do
                {
                    bytesRead = decompressedReader.Read(buffer, 0, buffer.Length);
                    fileWriter.Write(buffer, 0, bytesRead);
                }
                while (bytesRead > 0);

                decompressedReader.Close();
            }
            else
            {
                int bytesToRead = this.PhysicalDataSize;
                byte[] buffer = new byte[1024];
                int bytesRead = -1;
                do
                {
                    bytesRead = this.FileReader.Read(buffer, 0, Math.Min(buffer.Length, bytesToRead));
                    bytesToRead -= bytesRead;
                    fileWriter.Write(buffer, 0, bytesRead);
                }
                while ((bytesRead > 0) && (bytesToRead > 0));
            }
        }

        /// <summary>
        /// Reads the actual data for this section from the file.
        /// </summary>
        protected override void ReadSection()
        {
            if (this.SectionHasBeenRead)
            {
                return;
            }

            // Seek to the start of the data area for this section.
            long currentPosition = FileReader.BaseStream.Position;
            FileReader.BaseStream.Seek(this.StartPositionInFile + CommonSectionHeader.HeaderSize, System.IO.SeekOrigin.Begin);

            // Read the properties from the file.
            this.reserved = FileReader.ReadBytes(10);

            byte[] guidBytes = FileReader.ReadBytes(16);
            this.directoryEntryId = new Guid(guidBytes);

            this.featureBlockNumber = FileReader.ReadInt16();
            this.dataOffset = FileReader.ReadInt32();
            
            this.dataSize = FileReader.ReadUInt16();
            this.physicalDataSize = FileReader.ReadUInt16();

            this.startPositionOfData = FileReader.BaseStream.Position;

            this.SectionHasBeenRead = true;
            FileReader.BaseStream.Seek(currentPosition, System.IO.SeekOrigin.Begin);
        }
    }
}
